{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Optimize - The Synthesis Engine"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the previous chapter we focused on how to design quantum algorithms with the Qmod language. Once a quantum algorithm is designed, it can be compiled into a quantum circuit implementation that can be executed on quantum computers or simulators. This is done by the Classiq synthesis engine. In this chapter we cover how to use and utilize the Classiq synthesis engine in the IDE and the Python SDK through concrete examples."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Synthesis Background"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In most cases, a specific quantum model can be compiled to many, sometime infinite number of quantum circuits that might differ in their properties. Some will have more qubits with smaller circuit depth, some will have all the qubits connected to each other whilst other will not, and some will have a fewer number of 2-qubit gates than others. \n",
    "\n",
    "The Classiq synthesis engine receives the quantum model as an input, together with the constraints and preferences of the desired quantum program, and outputs a quantum program implementation of the quantum model that satisfies the constraints and preferences.\n",
    "\n",
    "Some of the current available constraints options for the Synthesis engine are:\n",
    "* Optimization parameter - either to optimize for circuit width or circuit depth;\n",
    "* Maximal gate count - maximum allowed number of a specific 1- or 2-qubit gate;\n",
    "* Maximal circuit width or circuit depth.\n",
    "\n",
    "And some of the available preferences:\n",
    "* Compiling the quantum circuit for a specific quantum processor;\n",
    "* The desired connectivity map of the quantum circuit;\n",
    "* The output format of the quantum circuit, e.g. `QASM` or `QIR`.\n",
    "\n",
    "A full list of possible [constraints](https://docs.classiq.io/latest/classiq_101/classiq_concepts/optimize/) and [preferences](https://docs.classiq.io/latest/reference-manual/platform/synthesis/constraints/) is available in the reference manual. \n",
    "\n",
    "In the following we cover how to apply constraints and preferences through a concrete example."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Concrete Example"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's go back to the example covered in the 'Design - The Qmod language' tutorial, where the task is to create a quantum algorithm that calculates in a superposition the arithmetic expression $y=x^2+1$. \n",
    "\n",
    "The following model written in Qmod implements the desired task:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:26:59.358723Z",
     "iopub.status.busy": "2024-05-07T13:26:59.358262Z",
     "iopub.status.idle": "2024-05-07T13:27:02.443486Z",
     "shell.execute_reply": "2024-05-07T13:27:02.442825Z"
    }
   },
   "outputs": [],
   "source": [
    "from classiq import *\n",
    "\n",
    "\n",
    "@qfunc\n",
    "def main(x: Output[QNum], y: Output[QNum]):\n",
    "\n",
    "    allocate(4, x)\n",
    "    hadamard_transform(x)  # creates a uniform superposition\n",
    "    y |= x**2 + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Which one can always directly synthesize without any constraints or preferences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:02.446891Z",
     "iopub.status.busy": "2024-05-07T13:27:02.446063Z",
     "iopub.status.idle": "2024-05-07T13:27:04.755389Z",
     "shell.execute_reply": "2024-05-07T13:27:04.754635Z"
    }
   },
   "outputs": [],
   "source": [
    "quantum_program = synthesize(create_model(main))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div  style=\"text-align:center;\">\n",
    "    <img src=\"https://docs.classiq.io/resources/design.gif\" style=\"width:100%;\">\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But in the following we cover how to do apply constraints and preferences. We first cover how to apply these in the IDE and then in the Python SDK."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Constraints and Preferences in the IDE"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the IDE, once your model is completed, you can directly synthesize your algorithm with the default constraints and preferences by pressing the `Synthesize` button:\n",
    "\n",
    "\n",
    "<div  style=\"text-align:center;\" >\n",
    "    <img src=\"https://docs.classiq.io/resources/optimize_default.png\" style=\"width:100%;\">\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, if you do want to apply some constraints and preferences, this can be done easily by adapting the parameters on the right-hand side of the screen and then synthesizing your model:\n",
    "\n",
    "<div  style=\"text-align:center;\" >\n",
    "    <img src=\"https://docs.classiq.io/resources/optimize_adapt.png\" style=\"width:100%;\">\n",
    "</div>\n",
    "\n",
    "All the constraints and preferences that are available in the IDE can also be configured in the Python SDK as we cover next."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Constraints and Preferences in the Python SDK "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The synthesis engine receives an underlying Qmod representation of the quantum model that is constructed in the Python SDK using the `create_model` function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:04.759889Z",
     "iopub.status.busy": "2024-05-07T13:27:04.759621Z",
     "iopub.status.idle": "2024-05-07T13:27:04.769643Z",
     "shell.execute_reply": "2024-05-07T13:27:04.768869Z"
    }
   },
   "outputs": [],
   "source": [
    "quantum_model = create_model(main)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This `quantum_model` can be directly synthesized with the command `synthesize(quantum_model)` to return the quantum program implementation. However, if we want to apply some constraints and preferences we need to adapt the `quantum_model` representation.\n",
    "\n",
    "First, assume that we want to receive a circuit implementation with minimal number of qubits and with maximal circuit depth of $500$. So we can apply these as constraints to our `quantum_model`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:04.774439Z",
     "iopub.status.busy": "2024-05-07T13:27:04.773221Z",
     "iopub.status.idle": "2024-05-07T13:27:04.782112Z",
     "shell.execute_reply": "2024-05-07T13:27:04.781399Z"
    }
   },
   "outputs": [],
   "source": [
    "quantum_model_with_constraints = set_constraints(\n",
    "    quantum_model, Constraints(optimization_parameter=\"width\", max_depth=500)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then the `quantum_model` can be then synthesized as usual:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:04.787739Z",
     "iopub.status.busy": "2024-05-07T13:27:04.785700Z",
     "iopub.status.idle": "2024-05-07T13:27:08.003675Z",
     "shell.execute_reply": "2024-05-07T13:27:08.002741Z"
    }
   },
   "outputs": [],
   "source": [
    "quantum_program = synthesize(quantum_model_with_constraints)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And the parameters of the circuit implementation can be extracted:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:08.009277Z",
     "iopub.status.busy": "2024-05-07T13:27:08.008746Z",
     "iopub.status.idle": "2024-05-07T13:27:08.026098Z",
     "shell.execute_reply": "2024-05-07T13:27:08.025197Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The circuit width is 12 and the circuit_depth is 392\n"
     ]
    }
   ],
   "source": [
    "circuit_width = QuantumProgram.from_qprog(quantum_program).data.width\n",
    "circuit_depth = QuantumProgram.from_qprog(quantum_program).transpiled_circuit.depth\n",
    "print(f\"The circuit width is {circuit_width} and the circuit_depth is {circuit_depth}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary> Compilation vs. Transpilation </summary>\n",
    "The synthesis engine is a compiler that compiles a high-level functional model to one specific circuit out of many possible implementations. A transpiler on the other hand, transforms one circuit implementation to another one. Its use can be to change from a circuit representation with a given basis gate set to another one, or to further optimize a given circuit implementation with basic optimization procedures like cancellation of two identical Hermitian gates applied consequently.  \n",
    "</details>\n",
    "\n",
    "It is **highly recommended** to complete the following exercise in order to experience by yourself for the first time how the same quantum algorithm can be compiled into two different circuit implementation that can substantially differ from each other.\n",
    "\n",
    "<details>\n",
    "<summary> Recommended Exercise </summary>\n",
    "Modify the constraints above such that you optimize the circuit for minimal circuit depth using maximum 25 qubits. What are the circuit depth and width you receive? Are they different than the above example? Try to analyze the two quantum circuits using `show(quantum_program)` and figure our which functional building block is implemented differently. \n",
    "</details>\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Assume now that we want to execute the above quantum model on the IBM quantum processor `ibm_brisbane`. It is recommended to pass this information to the synthesis engine so the quantum program will be most adequate for the desired quantum computer. This is done by adding the following preference and re-synthesizing `quantum_model`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:08.077340Z",
     "iopub.status.busy": "2024-05-07T13:27:08.076738Z",
     "iopub.status.idle": "2024-05-07T13:27:19.377781Z",
     "shell.execute_reply": "2024-05-07T13:27:19.376957Z"
    }
   },
   "outputs": [],
   "source": [
    "quantum_model_with_preferences = set_preferences(\n",
    "    quantum_model,\n",
    "    Preferences(backend_service_provider=\"IBM Quantum\", backend_name=\"ibm_brisbane\"),\n",
    ")\n",
    "\n",
    "quantum_program = synthesize(quantum_model_with_preferences)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary> Optional Exercise </summary>\n",
    "Extract the circuit depth and width of the above quantum program. How do these differ from the previous values? Try to think why they differ in such a way. It is good to know that the `IBM Brisbane` device as specific limited connectivity between its qubits, so there might be a certain overhead in applying some 2-qubit gates...\n",
    "</details>\n",
    "\n",
    "So now you have a quantum program that implements the quantum model that calculates $y=x^2+1$ in a superposition, that is optimized for a specific real quantum computer. It only remains to follow the next chapters and to see how to actually run it on that computer with Classiq. But first we want to deep dive to the Analysis capabilities of Classiq."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-05-07T13:27:19.383085Z",
     "iopub.status.busy": "2024-05-07T13:27:19.381845Z",
     "iopub.status.idle": "2024-05-07T13:27:19.391736Z",
     "shell.execute_reply": "2024-05-07T13:27:19.390966Z"
    }
   },
   "outputs": [],
   "source": [
    "write_qmod(quantum_model_with_constraints, \"optimize\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.11.7 ('classiq')",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  },
  "vscode": {
   "interpreter": {
    "hash": "529b62266d4f537a408698cf820854c65fe877011c7661f0f70aa11c4383fddc"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
